/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021.
 * Description: support r/w llc register with djtag
 * Author: qiuguorui
 * Create: 2021-01-05
 */
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/regmap.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/platform_data/hisi-djtag.h>

#define  BANK_NUM_PER_LLC_MAX           5
#define  L3T_ID_MAX                     8
#define  BANK_NUM_PER_DIE 				4
/*
 * we define op mode in dts:
 * MODE_DJTAG_V1    -- 0
 * MODE_DJTAG_V2    -- 1
 * MODE_DJTAG_V2_EX -- 2
 * MODE_PHY_ADDR    -- 3
 */
enum reg_op_mode {
	MODE_DJTAG_V1,
	MODE_DJTAG_V2,
	MODE_DJTAG_V2_EX,
	MODE_PHY_ADDR
};

static struct of_device_id llc_of_match[] = {
	{ .compatible = "hisilicon,hip05-llc"},
	{ .compatible = "hisilicon,hi1382-llc"},
	{ .compatible = "hisilicon,sd6801-llc"},
	{ .compatible = "hisilicon,hi1213-llc"},
	{ .compatible = "hisilicon,hi1383-llc"},
	{},
};

MODULE_DEVICE_TABLE(of, llc_of_match);

/*
 * hi1382 has 4 banks per llc
 * hi1213 has 2 banks per llc
 * hi1383 has 1 l3t per llc, we treat
 * an l3t as an llc which contains only 1 bank,
 * which is set as default.
 */
static int bank_num_per_llc = 1;

/*
 * multi sccl soc has more than one llc,
 * such as hi1616
 */
static unsigned int llc_num;

#define MAX_DIE_NUM 4
int dieid_arr[MAX_DIE_NUM] = {-1, -1, -1, -1};
static LIST_HEAD(llc_list);

struct djtag_cfg_ops {
	u32 (*read_one)(struct device_node *node, u32 offset,
				int bank);
	void (*write_one)(struct device_node *node, u32 offset,
				u32 value, int bank);
	void (*write_all)(struct device_node *node, u32 offset,
			u32 value);
};

struct l3t_data {
	void __iomem *base;
	u32 id;
};

static u32 l3t_readl(void __iomem *base, u32 offset)
{
	if (!base) {
		pr_err("L3T: [%p] read 0x%x failed!\n", base, offset);
		return 0;
	}

	return readl(base + offset);
};

static void l3t_writel(void __iomem *base, u32 offset, u32 val)
{
	if (!base) {
		pr_err("L3T: [%pK] write 0x%x(0x%x) failed!\n",
				base, offset, val);
		return;
	}

	writel(val, base + offset);
};

struct llc_of_data {
	struct list_head list;
	struct device_node *node;
	enum reg_op_mode mode;
	int dieid;
	struct l3t_data l3t;
	const struct djtag_cfg_ops *cfg_ops;
};

static inline int num_present_llcs(void)
{
	return !list_empty(&llc_list);
}

#define ID_SIZE 4
#define HHA_BANK_NUM 2

/*
 * LLC0 0x01
 * LLC1 0x02
 * LLC2 0x00
 * LLC3 0x03
 */
static const int hip05_llc_id_convert[ID_SIZE] = {0x1, 0x2, 0x0, 0x3};

static u32 hip05_read_one(struct device_node *node, u32 offset, int bank)
{
	u32 val;
	int ret;
	int chain_id;

	chain_id = hip05_llc_id_convert[bank];

	ret = djtag_readl(node, offset, 0x4, chain_id, &val);
	if (ret) {
		pr_err("LLC: %s read 0x%x failed!\n", node->full_name, offset);
		return 0;
	}

	return val;
}

static void hip05_write_one(struct device_node *node, u32 offset, u32 val, int bank)
{
	int ret;
	u32 mod_mask;

	mod_mask = 0x1U << hip05_llc_id_convert[bank];
	ret = djtag_writel(node, offset, 0x4, mod_mask, val);
	if (ret)
		pr_err("LLC: %s write 0x%x %d failed!\n", node->full_name,
				offset, bank);
}

static void hip05_write_all(struct device_node *node, u32 offset, u32 val)
{
	int ret;

	ret = djtag_writel(node, offset, 0x4, 0, val);
	if (ret)
		pr_err("LLC: %s write 0x%x(all) failed!\n", node->full_name, offset);
};

/*
 * LLC0 ---- 8h01(0x01)
 * LLC1 ---- 8h02(0x02)
 * LLC2 ---- 8h03(0x03)
 * LLC3 ---- 8h04(0x04)
 * HHA0 ---- 8h10(0x10)
 * HHA1 ---- 8h20(0x20)
 */
static const int hi1382_llc_id_convert[ID_SIZE + HHA_BANK_NUM] = {
	0x1, 0x2, 0x3, 0x4, 0x10, 0x20
};

static u32 hi1382_read_one(struct device_node *node, u32 offset, int bank)
{
	u32 val;
	int ret;
	int mod_id;

	mod_id = hi1382_llc_id_convert[bank];
	ret = djtag_readl(node, offset, mod_id, 0, &val);
	if (ret) {
		pr_err("LLC: %s read 0x%x failed!\n", node->full_name, offset);
		return 0;
	}

	return val;
}

static void hi1382_write_one(struct device_node *node, u32 offset, u32 val, int bank)
{
	int ret;
	int mod_id;

	mod_id = hi1382_llc_id_convert[bank];
	ret = djtag_writel(node, offset, mod_id, 0, val);
	if (ret)
		pr_err("LLC: %s write 0x%x %d failed!\n", node->full_name,
				offset, bank);
}

static void hi1382_write_all(struct device_node *node, u32 offset, u32 val)
{
	int ret, mod_id, i;

	ret = 0;
	for (i = 0; i < bank_num_per_llc; i++) {
		mod_id = hi1382_llc_id_convert[i];
		ret += djtag_writel(node, offset, mod_id, 0, val);
	}
	if (ret)
		pr_err("LLC: %s write 0x%x(all) failed!\n", node->full_name, offset);
};

static u32 sd6801_read_one(struct device_node *node, u32 offset, int bank)
{
	u32 val;
	int ret;
	int chain_id;

	chain_id = bank;
	ret = djtag_readl(node, offset, 0x1, chain_id, &val);
	if (ret) {
		pr_err("LLC: %s read 0x%x failed!\n", node->full_name, offset);
		return 0;
	}

	return val;
}

static void sd6801_write_one(struct device_node *node, u32 offset, u32 val, int bank)
{
	int ret;
	int mod_mask;

	mod_mask = 0x1U << bank;
	ret = djtag_writel(node, offset, 0x1, mod_mask, val);
	if (ret)
		pr_err("LLC: %s write 0x%x %d failed!\n", node->full_name,
				offset, bank);
}

static void sd6801_write_all(struct device_node *node, u32 offset, u32 val)
{
	int ret;

	ret = djtag_writel(node, offset, 0x1, 0, val);
	if (ret)
		pr_err("LLC: %s write 0x%x(all) failed!\n", node->full_name, offset);
};

/* djtag cfg v1 */
static const struct djtag_cfg_ops hip05_cfg_ops = {
	.read_one	= hip05_read_one,
	.write_one	= hip05_write_one,
	.write_all	= hip05_write_all,
};

/* djtag cfg v2 */
static const struct djtag_cfg_ops hi1382_cfg_ops = {
	.read_one	= hi1382_read_one,
	.write_one	= hi1382_write_one,
	.write_all	= hi1382_write_all,
};

/* djtag cfg v2 extend */
static const struct djtag_cfg_ops sd6801_cfg_ops = {
	.read_one	= sd6801_read_one,
	.write_one	= sd6801_write_one,
	.write_all	= sd6801_write_all,
};

/* MNT operation regs */
#define LLC_MAINT_CTRL		0x20
#define MNT_RANGE_SHIFT		0x3
#define MNT_RANGE_GLOBAL	(0x0U << MNT_RANGE_SHIFT)
#define MNT_TYPE_SHIFT		0x1
#define MNT_EN_SHIFT		0x0
#define MNT_EN				(0x1U << MNT_EN_SHIFT)

#define L3T_MAINT_CTRL		0x0420

enum mnt_type {
	MNT_TYPE_CLEAN  = 0x1,
	MNT_TYPE_INV    = 0x2,
	MNT_TYPE_FLUSH  = 0x3,
};

static DEFINE_SPINLOCK(llc_maint_lock);
/*
 * do LLC maintain operation by type via phy_addr
 * type: MNT_TYPE_CLEAN/MNT_TYPE_INV/MNT_TYPE_FLUSH
 */
static void llc_mnt_by_phy_addr(struct llc_of_data *llc_data, enum mnt_type type)
{
	u32 val;
	struct l3t_data *l3t = &(llc_data->l3t);
	u32 tmp = (type == MNT_TYPE_FLUSH) ? 0x0 : type;

	val = MNT_RANGE_GLOBAL | (tmp << MNT_TYPE_SHIFT) | MNT_EN;
	l3t_writel(l3t->base, L3T_MAINT_CTRL, val);
	/* wait LLC maintain operation to be completed */
	while (l3t_readl(l3t->base, L3T_MAINT_CTRL) & MNT_EN)
		cpu_relax();
}
/*
 * do LLC maintain operation by type via jtag
 * type: MNT_TYPE_CLEAN/MNT_TYPE_INV/MNT_TYPE_FLUSH
 */
static void llc_mnt_by_jtag(struct llc_of_data *llc_data, enum mnt_type type)
{
	u32 val;
	int i;
	u32 tmp = type;

	val = MNT_RANGE_GLOBAL | (tmp << MNT_TYPE_SHIFT) | MNT_EN;
	llc_data->cfg_ops->write_all(llc_data->node, LLC_MAINT_CTRL, val);
	/* wait LLC maintain operation to be completed */
	for (i = 0; i < bank_num_per_llc; i++)
		while (llc_data->cfg_ops->read_one(llc_data->node,
					LLC_MAINT_CTRL, i) & MNT_EN)
			cpu_relax();
}
/*
 * select ops mode by llc_data->mode, djtag or phy_addr
 * mode: MODE_DJTAG_V1/MODE_DJTAG_V2/MODE_DJTAG_V2_EX/MODE_PHY_ADDR
 */
static void do_llc_ops(struct llc_of_data *llc_data, enum mnt_type type)
{
	if (llc_data->mode == MODE_PHY_ADDR)
		llc_mnt_by_phy_addr(llc_data, type);
	else
		llc_mnt_by_jtag(llc_data, type);
}

static void llc_global_mnt(enum mnt_type type)
{
	struct llc_of_data *llc_data = NULL;
	struct llc_of_data *p = NULL;
	unsigned long flags;

	if (num_present_llcs() == 0) {
		pr_debug("No LLC present\n");
		return;
	}

	spin_lock_irqsave(&llc_maint_lock, flags);
	list_for_each_entry_safe(llc_data, p, &llc_list, list)
		do_llc_ops(llc_data, type);
	spin_unlock_irqrestore(&llc_maint_lock, flags);
}

void llc_clean_all(void)
{
	llc_global_mnt(MNT_TYPE_CLEAN);
}

void llc_flush_all(void)
{
	llc_global_mnt(MNT_TYPE_FLUSH);
}

bool flush_writerbuffer_fix_enabled;
#define HHA_WRBUFF_THRES		0x74
#define HHA_FLUSH_ALL			0x420804
#define HHA0_BANK_ID			4
#define HHA1_BANK_ID			5

static int write_hha_writebuffer(u32 offset, u32 bank, u32 value)
{
#ifdef CONFIG_RTOS_HAL_HI1382_FLUSH_WRITEBUFFER_FIX
	struct llc_of_data *llc_data = NULL;
	struct llc_of_data *p = NULL;
	int mod_id;
	int ret = 0;

	if ((bank < HHA0_BANK_ID) || (bank > HHA1_BANK_ID))
		return -EINVAL;

	mod_id = hi1382_llc_id_convert[bank];

	list_for_each_entry_safe(llc_data, p, &llc_list, list) {
		ret = djtag_writel(llc_data->node, offset, mod_id, 0, value);
		if (ret)
			return ret;
	}
#endif
	return 0;
}

void writer_buffer_flush(void)
{
	int ret;

	if (!flush_writerbuffer_fix_enabled)
		return;

	ret = write_hha_writebuffer(HHA_WRBUFF_THRES, HHA0_BANK_ID, HHA_FLUSH_ALL);
	if (ret)
		printk("[write buffer] flush BANK 4 fail\n");

	ret = write_hha_writebuffer(HHA_WRBUFF_THRES, HHA1_BANK_ID, HHA_FLUSH_ALL);
	if (ret)
		printk("[write buffer] flush BANK 5 fail\n");
}
EXPORT_SYMBOL(writer_buffer_flush);

int read_l3cache(u32 offset, u32 bank, u32 *value)
{
	struct llc_of_data *llc_data = NULL;
	struct llc_of_data *p = NULL;
	int dieid;
	enum reg_op_mode mode;
	u32 bank_max;
	u32 bank_t = bank;
	struct l3t_data *l3t = NULL;

	if (!value) {
		pr_err("value's addr is null,please check!\n");
		return -EINVAL;
	}

	bank_max = llc_num * bank_num_per_llc;
	bank_t = bank % BANK_NUM_PER_DIE;

	if (bank >= bank_max || bank < 0)
		return -EINVAL;

	dieid = dieid_arr[bank / BANK_NUM_PER_DIE];
	if (num_present_llcs() == 0) {
		pr_err("WARNING! No LLC present\n");
		return -ENODEV;
	}

	list_for_each_entry_safe(llc_data, p, &llc_list, list) {
		mode = llc_data->mode;
		if (mode == MODE_PHY_ADDR) {
			l3t = &(llc_data->l3t);
			if (l3t->id == bank) {
				*value = l3t_readl(l3t->base, offset);
				break;
			}
		} else {
			if (llc_data->dieid == dieid) {
				*value = llc_data->cfg_ops->read_one(llc_data->node, offset, bank_t);
				break;
			}
		}
	}

	return 0;
}
EXPORT_SYMBOL(read_l3cache);

int write_l3cache(u32 offset, u32 bank, u32 value)
{
	struct llc_of_data *llc_data = NULL;
	struct llc_of_data *p = NULL;
	struct l3t_data *l3t = NULL;
	enum reg_op_mode mode;
	int dieid;
	/* fiq will use bank 4 and bank 5,so we set to 5 */
	u32 bank_max;
	u32 bank_t = bank;
	pr_info("LLC:bank=%d,offset=0x%x,value=0x%x\n", bank, offset, value);
	bank_max = llc_num * bank_num_per_llc;
	bank_t = bank % BANK_NUM_PER_DIE;

	if (bank > bank_max) {
		pr_err("please check, input bank(%u) > bank_max(%u)\n", bank, bank_max);
		return -EINVAL;
	}

	dieid = dieid_arr[bank / BANK_NUM_PER_DIE];
	if (num_present_llcs() == 0) {
		pr_err("WARNING! No LLC present\n");
		return -ENODEV;
	}

	list_for_each_entry_safe(llc_data, p, &llc_list, list) {
		mode = llc_data->mode;
		if (mode == MODE_PHY_ADDR) {
			l3t = &(llc_data->l3t);
			if (l3t->id == bank) {
				l3t_writel(l3t->base, offset, value);
				break;
			}
		} else {
			if (llc_data->dieid == dieid) {
				llc_data->cfg_ops->write_one(llc_data->node, offset, value, bank_t);
				break;
			}
		}
	}

	return 0;
}
EXPORT_SYMBOL(write_l3cache);

int get_present_llcs_num(void)
{
	return num_present_llcs();
}
EXPORT_SYMBOL(get_present_llcs_num);

int get_llcs_banks_total(void)
{
	/* llc_num <= 8  && bank_num_per_llc <= 8 */
	return llc_num * bank_num_per_llc;
}
EXPORT_SYMBOL(get_llcs_banks_total);

static int llc_set_ops(struct llc_of_data *llc_data)
{
	switch (llc_data->mode) {
	case MODE_DJTAG_V1:
		llc_data->cfg_ops = &hip05_cfg_ops;
		break;
	case MODE_DJTAG_V2:
		llc_data->cfg_ops = &hi1382_cfg_ops;
		break;
	case MODE_DJTAG_V2_EX:
		llc_data->cfg_ops = &sd6801_cfg_ops;
		break;
	case MODE_PHY_ADDR:
		break;
	}

	if (!llc_data->cfg_ops)
		return -EINVAL;

	return 0;
}

static int llc_legacy_probe(struct platform_device *pdev,
								struct llc_of_data *llc_data)
{
	int ret;
	int i;
	struct of_phandle_args arg;
	struct device *dev = &pdev->dev;
	struct device_node *node = dev->of_node;

	ret = of_parse_phandle_with_fixed_args(node,
			"djtag", 1, 0, &arg);
	if (ret) {
		pr_err("LLC: %s set error.\n", node->full_name);
		return -EINVAL;
	}

#ifdef CONFIG_RTOS_HAL_HI1382_FLUSH_WRITEBUFFER_FIX
	if (of_property_read_bool(node, "hisi1382,flush-writebuffer"))
		flush_writerbuffer_fix_enabled = true;
#endif
	/* die id must be between 0 and 9 */
	if (arg.args[0] > 0 && arg.args[0] < 9)
		llc_data->dieid = arg.args[0];
	llc_data->node = arg.np;

	ret = llc_set_ops(llc_data);
	if (ret)
		return ret;

	/* only MODE_PHY_ADDR mode can ignore l3_bank_num is dts */
	ret = of_property_read_u32(node, "l3_bank_num", &bank_num_per_llc);
	if (ret && llc_data->mode != MODE_PHY_ADDR) {
		pr_err("no l3_bank_num, must be error, check your dts\n");
		return -EINVAL;
	}
	if (bank_num_per_llc >= BANK_NUM_PER_LLC_MAX) {
		pr_err("l3_bank_num must be error, check your dts\n");
		return -EINVAL;
	}
	for (i = 0; i < ARRAY_SIZE(dieid_arr); i++) {
		if (dieid_arr[i] == -1) {
			printk("llc_data->dieid=%d, i=%d\n", llc_data->dieid, i);
			dieid_arr[i] = llc_data->dieid;
			break;
		}
	}
	return 0;
}

static int llc_normal_probe(struct platform_device *pdev,
				struct llc_of_data *llc_data)
{
	int ret;
	struct resource *res = NULL;
	struct l3t_data *l3t = NULL;
	struct device *dev = &pdev->dev;
	struct device_node *node = dev->of_node;

	l3t = &(llc_data->l3t);
	ret = of_property_read_u32(node, "l3tid", &(l3t->id));
	if (ret) {
		pr_err("LLC: not find l3tid feature!\n");
		return -EINVAL;
	}
	if (l3t->id >= L3T_ID_MAX) {
		pr_err("LLC: l3tid=%d > %d is error, check your dts\n", l3t->id, L3T_ID_MAX);
		return -EINVAL;
	}
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		pr_err("LLC: res is null\n");
		return -ENODEV;
	}
	l3t->base = ioremap(res->start, resource_size(res));
	if (!l3t->base) {
		pr_err("LLC: l3t#%d ioremap error!\n", l3t->id);
		return -ENOMEM;
	}

	return 0;
}

static int llc_dev_probe(struct platform_device *pdev)
{
	int ret;
	int op_mode;
	struct llc_of_data *llc_data = NULL;
	const struct of_device_id *of_id = NULL;
	struct device *dev = &pdev->dev;
	struct device_node *node = dev->of_node;

	of_id = of_match_device(llc_of_match, dev);
	if (!of_id)
		return -ENODEV;

	llc_data = kzalloc(sizeof(struct llc_of_data), GFP_KERNEL);
	if (!llc_data)
		return -ENOMEM;

	ret = of_property_read_u32(node, "op-mode", &op_mode);
	if (ret) {
		pr_err("no op-mode, please check your dts\n");
		return -EINVAL;
	}
	llc_data->mode = op_mode;
	switch (op_mode) {
	case MODE_DJTAG_V1:
	case MODE_DJTAG_V2:
	case MODE_DJTAG_V2_EX:
		ret = llc_legacy_probe(pdev, llc_data);
		break;
	case MODE_PHY_ADDR:
		ret = llc_normal_probe(pdev, llc_data);
		break;
	default:
		pr_err("invalid op-mode,please check dts\n");
		ret = -EINVAL;
		break;
	}
	if (ret)
		goto free;

	INIT_LIST_HEAD(&llc_data->list);
	list_add_tail(&llc_data->list, &llc_list);
	llc_num++;

	return 0;
free:
	kfree(llc_data);
	llc_data = NULL;
	return -EINVAL;
}

static int llc_dev_remove(struct platform_device *pdev)
{
	struct llc_of_data *tmp = NULL;
	struct llc_of_data *p = NULL;
	struct l3t_data *l3t = NULL;

	list_for_each_entry_safe(tmp, p, &llc_list, list) {
		l3t = &(tmp->l3t);
		if (l3t->base)
			iounmap(l3t->base);

		list_del(&tmp->list);
		kfree(tmp);
	}

	return 0;
}

static struct platform_driver llc_dev_driver = {
	.driver = {
		.name = "Hisilicon-LLC",
		.of_match_table = llc_of_match,
	},
	.probe = llc_dev_probe,
	.remove = llc_dev_remove,
};

module_platform_driver(llc_dev_driver);

MODULE_DESCRIPTION("Hisilicon LLC register ops");
MODULE_LICENSE("GPL");
